#! /usr/bin/perl -w

# ORM compiler

use strict;
use Getopt::Std;
use XML::Parser;
use File::Path;
use Cwd;


my $template_header = 
	"#ifndef EVEREST_ORM_GENERATED_^{GUARD}\n". 
	"#define EVEREST_ORM_GENERATED_^{GUARD}\n\n". 
	"#include <everest/dbc/dbc.h>\n".
	"#include <vector>\n#include <everest-orm.h>\n".
	"^{BEGIN_NAMESPACE}\n".
	"using namespace std;\n".
	"using namespace everest::dbc;\n".
	"using namespace everest::orm;\n\n".
	"\tclass ^{CLASS};\n\n".
	"\tclass ^{DAO_CLASS} : public everest::orm::DataAccessObject\n\t{\n".
	"\tpublic:\n".
	"\t\tstatic const char *s_insertStmt;\n".
	"\t\tstatic const char *s_selectStmtById;\n".
	"\t\tstatic const char *s_deleteStmtById;\n".
	"\t\tstatic const char *s_updateStmtById;\n".
	"\t\tstatic const char *s_selectStmt;\n\n".
	"\tpublic:\n".
	"\t\t^{DAO_CLASS}(everest::dbc::IConnection *pconn, size_t bufsize);\n".
	"\t\tvirtual ~^{DAO_CLASS}();\n\n".
	"\t\tvoid clear();\n".
	"\t\tint flush();\n".
	"\t\tint insert(const ^{CLASS}& obj);\n".
	"\t\tint update(const ^{CLASS}& obj);\n".
	"\t\tint remove(const ^{CLASS}& obj);\n".
	"\t\tint select(^{CLASS}& obj);"."\n".
	"\t\tint select(std::vector<^{CLASS}>& vec, const char *pszWhere);\n".
	"\n\tprotected:\n".
	"\t\tbool m_hasInsert;\n".
	"\t\tbool m_hasUpdate;\n".
	"\t\tbool m_hasDelete;\n".
	"\t\totl_nocommit_stream  m_insertStream;\n".
	"\t\totl_nocommit_stream  m_updateStream;\n".
	"\t\totl_nocommit_stream  m_deleteStream;\n".
	"\t}; // End of class ^{DAO_CLASS}\n".
	"^{END_NAMESPACE}\n".
	"#endif // EVEREST_ORM_GENERATED_^{GUARD}\n";

my $template_cpp = 
	"#include \"^{CLS_HEADER}\"\n".
	"#include \"^{DAO_CLASS}.h\"\n\n".
	"^{BEGIN_NAMESPACE}\n\n".
	"const char *^{DAO_CLASS}::s_insertStmt = \"insert into ^{TABLE} (^{FIELDS}) values (^{INSERT_VALUES_HOLDER})\";\n".
	"const char *^{DAO_CLASS}::s_selectStmt = \"select ^{FIELDS} from ^{TABLE} \";\n".
	"const char *^{DAO_CLASS}::s_selectStmtById = \"select ^{FIELDS_NO_PK} from ^{TABLE} where ^{FIELD_PK}=^{SELECT_VALUE_PK_HOLDER}\";\n".
	"const char *^{DAO_CLASS}::s_updateStmtById = \"update ^{TABLE} set ^{UPDATE_SET_NO_PK_HOLDER} where ^{FIELD_PK}=^{UPDATE_PK_HOLDER}\";\n".
	"const char *^{DAO_CLASS}::s_deleteStmtById = \"delete from ^{TABLE} where ^{FIELD_PK}=^{DELETE_VALUE_PK_HOLDER}\";\n\n".
	"^{DAO_CLASS}::^{DAO_CLASS}(otl_connect *pconn, size_t bufsize)\n".
	"\t: DataAccessObject(pconn, bufsize), m_hasInsert(false), m_hasUpdate(false), m_hasDelete(false){}\n\n".
	"^{DAO_CLASS}::~^{DAO_CLASS}(){ this->clear(); }\n\n".
	"void ^{DAO_CLASS}::clear() {\n".
	"\tif (m_hasInsert) this->m_insertStream.close();\n".
	"\tif (m_hasUpdate) this->m_updateStream.close();\n".
	"\tif (m_hasDelete) this->m_deleteStream.close();\n".
	"\tthis->m_hasInsert = false;\n".
	"\tthis->m_hasUpdate = false;\n".
	"\tthis->m_hasDelete = false;\n}\n\n".
	"int  ^{DAO_CLASS}::flush() {\n".
	"\tif ( m_hasInsert ) m_insertStream.flush();\n".
	"\tif ( m_hasUpdate ) m_updateStream.flush();\n".
	"\tif ( m_hasDelete ) m_deleteStream.flush();\n}\n\n".
	"int ^{DAO_CLASS}::insert(const ^{CLASS}& obj) {\n".
	"\tif ( !m_hasInsert ) m_insertStream.open(m_bufsize, s_insertStmt, *m_conn);\n".
	"\tm_insertStream^{STREAM_OUT_FIELDS_ALL};\n".
	"\tm_hasInsert = true;\n\treturn 1;\n}\n\n".
	"int ^{DAO_CLASS}::update(const ^{CLASS}& obj) {\n".
	"\tif ( !m_hasUpdate ) m_updateStream.open(m_bufsize, s_updateStmtById, *m_conn);\n".
	"\tm_updateStream^{STREAM_OUT_FIELDS_NO_PK}^{STREAM_OUT_FIELDS_PK};\n".
	"\tm_hasUpdate = true;\n\treturn 1;\n}\n\n".
	"int ^{DAO_CLASS}::remove(const ^{CLASS}& obj) {\n".
	"\tif ( !m_hasDelete ) m_deleteStream.open(m_bufsize, s_deleteStmtById, *m_conn);\n".
	"\tm_deleteStream^{STREAM_OUT_FIELDS_PK};\n".
	"\tm_hasDelete = true;\n\treturn 1;\n}\n\n".
	"int ^{DAO_CLASS}::select(^{CLASS}& obj) {\n".
	"\tint result = 0;\n".
	"\totl_nocommit_stream os(m_bufsize, s_selectStmtById, *m_conn);\n".
	"\tos^{STREAM_OUT_FIELDS_PK};\n".
	"\tif (!os.eof() ) { \n".
	"\t\t^{GET_VALUES_FROM_STREAM_NO_PK}".
	"\t\tresult = 1; }\n else {\n\t\t result = 0;\n\t}\n".
	"\treturn result;\n}\n\n".
	"int ^{DAO_CLASS}::select(std::vector<Member>& vec, const char *pszWhere) {\n".
	"\tstd::string sql;\n\tsql.reserve(1024);\n\tsql.assign(s_selectStmt);\n".
	"\tif (pszWhere && pszWhere[0] != '\\0') {\n".
	"\t\tsql.append(\" where \").append(pszWhere);\n\t}".
	"\tint result = 0;\n".
	"\totl_nocommit_stream os(m_bufsize, sql.c_str(), *m_conn);\n".
	"\twhile (!os.eof()) {\n".
	"\t\tvec.push_back(^{CLASS}());\n".
	"\t\t^{CLASS}& obj = vec.back();\n".
	"\t\t^{VALUES_FROM_STREAM_ALL}".
	"\t\tresult += 1;\n\t\t}\n\treturn result;\n}\n".
	"^{END_NAMESPACE}\n\n";
	
sub print_help 
{
	print 'EverestORM class mapping compiler.'."\n";
	print "Usage: ormc [-d output_dir] <mapping_xml>\n";
	print "    output_dir   the directory where the generated file placed.\n";
	print "    mapping_xml  the ORM mapping xml file.\n\n";
}

sub create_output_dir 
{
	my $dir = shift;
	if ( defined($dir) && $dir ne '') { 
		File::Path::make_path($dir);
		return Cwd::abs_path(glob($dir));
	} else {
		return getcwd();
	}
}


# generate a string include all colname seperated by ','
sub generate_db_cols_string  
{
	my $props = shift;
	my $str = '';
	my $count = @{$props};
	
	if ( $count > 0 ) {
		$str = "${$props}[0]{column}";
	}
	my $first = 'true';
	foreach my $prop ( @{$props} ) {
		if ( $first eq 'true' ) { $first = 'false'; next; }
		$str = "$str,${$prop}{column}";
	}
	return $str;
}
# generate a string include colnames seperated by ',' without primary key
sub generate_db_cols_string_no_pk 
{
	my $props = shift;
	my $str = '';
	my $count = @{$props};
	
	my $first = 'true';
	foreach my $prop ( @{$props} ) {
		if ( ${$prop}{pk} eq 'true') { next; }
		
		if ( $first eq 'true' ) { 
			$str = ${$prop}{column};
			$first = 'false'; 
			next; 
		}
		$str = "$str,${$prop}{column}";
	}
	return $str;
}

sub get_db_col_pk    # get primary key col name
{
	my $props = shift;
	foreach my $prop ( @{$props} ) {
		if ( ${$prop}{pk} eq 'true') {
			return ${$prop}{column};
		}
	}
}

sub get_value_holders
{
	my ($props, $pk, $basecount) = @_;   # pk=1: only pk; pk=0:include pk; pk=-1:no pk
	my $str = '';
	my $first = 1;
	my $count = $basecount;
	
	foreach my $prop (@{$props}) {
		if ( ${$prop}{pk} eq 'true' and $pk < 0 ) { next; } # current is pk but no pk need
		if ( ${$prop}{pk} eq 'false' and $pk > 0) { next; } # current is not pk but only pk need
			
		if ( $first == 0 ) { $str = $str.',';}
		else { $first = 0; }

		my $col = ${$prop}{column};
		
		if (${$prop}{type} ~~ ['char','varchar']) {
			$str = $str." :$count<char";
			$str = $str."[${$prop}{size}]" if exists ${$prop}{size};
			$str = $str.">";
		} elsif (${$prop}{type} ~~ ['datetime', 'date', 'time', 'timestamp']) {
			$str = $str." :$count<timestamp>";
		} elsif (${$prop}{type} eq 'int' ) {
			$str = $str." :$count<int>";
		} elsif (${$prop}{type} ~~ ['number', 'decimal']) {
			$str = $str." :$count<double>";
		} else {
			$str = $str." :$count<${$prop}{type}>";
		}
		$count += 1;
	} # foreach
	
	return $str, $count;
}

sub get_value_update_set_holders
{
	my ($props, $pk, $basecount) = @_;   # pk=1: only pk; pk=0:include pk; pk=-1:no pk
	my $str = '';
	my $first = 1;
	my $count = $basecount;
	
	foreach my $prop (@{$props}) {
		if ( ${$prop}{pk} eq 'true' and $pk < 0 ) { next; } # current is pk but no pk need
		if ( ${$prop}{pk} eq 'false' and $pk > 0) { next; } # current is not pk but only pk need
			
		if ( $first == 0 ) { $str = $str.','; }
		else { $first = 0; }

		my $col =  ${$prop}{column};
		my $type = ${$prop}{type};
		my $size = 0;
		$size = ${$prop}{size} if exists ${$prop}{size};
		
		if ($type ~~ ['char','varchar']) {
			$str = $str." $col = :$count<char";
			$str = $str."[$size]" if $size > 0;
			$str = $str.">";
		} elsif ($type ~~ ['datetime', 'date', 'time', 'timestamp']) {
			$str = $str." $col = :$count<timestamp>";
		} elsif ($type ~~ ['int', 'int32'] ) {
			$str = $str." $col = :$count<int>";
		} elsif ($type ~~ ['number', 'decimal']) {
			$str = $str." $col = :$count<double>";
		} else {
			$str = $str." $col = :$count<$type>";
		}
		$count += 1;
	} # foreach
	
	return $str, $count;
}

sub get_stream_in_fields  # get <<prop_value1<<prop_value2 ...
{
	my ($props, $pk) = @_;  # pk=1: only pk; pk=0:include pk; pk=-1:no pk
	my $str = '';
	
	foreach my $prop (@{$props}) {
		if ( ${$prop}{pk} eq 'true' and $pk < 0 ) { next; } # current is pk but no pk need
		if ( ${$prop}{pk} eq 'false' and $pk > 0) { next; } # current is not pk but only pk need
			
		if (${$prop}{type} ~~ ['char','varchar']) {
			if (${$prop}{ctype} ne 'string') {
				$str = "$str<<convertToString(obj.get${$prop}{name}())";
			} else {
				$str = "$str<<obj.get${$prop}{name}()";
			}
		} elsif (${$prop}{type} ~~ ['datetime', 'date', 'time', 'timestamp']) {
			$str = "$str<<convertToDateTime(obj.get${$prop}{name}())";
		} elsif (${$prop}{type} eq 'int' ) {
			if (${$prop}{ctype} ~~ ['int', 'int32', 'int64', undef]) {
				$str = "$str<<obj.get${$prop}{name}()";
			} else {
				$str = "$str<<convertToString(obj.get${$prop}{name}())";
			}
		} elsif (${$prop}{type} ~~ ['number', 'decimal']) {
			if (${$prop}{ctype} ~~ ['int', 'int32', 'int64', 'float', 'double', undef]) {
				$str = "$str<<obj.get${$prop}{name}()";
			} else {
				$str = "$str<<convertToDecimal(obj.get${$prop}{name}())";
			}
		} else {
			$str = "$str<<obj.get${$prop}{name}()";
		}
	} # foreach
	
	return $str;
}

sub get_values_from_stream
{
	my ($props, $pk) = @_;  # pk=0:include pk; pk=-1:no pk 
	my $str = '';
	
	my $getfunc = 'none';
	my $convfunc = 'none';
	my $dbtype = 0;  # 1-varchar/char;2-int,3-number decimal;4-date/time/datetime/timestamp
	my $ctype  = 0;  # 1-string;2-int/int32;3-double/float;4-otl_datetime
	
	foreach my $prop (@{$props}) {
		if ( ${$prop}{pk} eq 'true' and $pk < 0 ) { next; } # current is pk but no pk need
		
		if ( ${$prop}{type} ~~ ['char', 'varchar']) {
			$getfunc = 'getStringFromStream(os)';
			$dbtype = 1;
		} elsif ( ${$prop}{type} ~~ ['int'] ) {
			$getfunc = 'getIntegerFromStream(os)';
			$dbtype = 2;
		} elsif ( ${$prop}{type} ~~ ['number','decimal'] ) {
			$getfunc = 'getDoubleFromStream(os)';
			$dbtype = 3;
		} elsif ( ${$prop}{type} ~~ ['date', 'time', 'datetime', 'timestamp'] ) {
			$getfunc = 'getDateTimeFromStream(os)';
			$dbtype  = 4;
		} else {
			$getfunc = 'none';
			$dbtype = 0;
		}
		
		if ( ${$prop}{ctype} ~~ ['string']) {
			$convfunc = 'convertToString';
			$ctype = 1;
		} elsif ( ${$prop}{ctype} ~~ ['int int32'] ) {
			$convfunc = 'convertToInteger';
			$ctype = 2;
		} elsif ( ${$prop}{ctype} ~~ ['float double'] ) {
			$convfunc = 'convertToDecimal';
			$ctype = 3;
		} elsif ( ${$prop}{ctype} ~~ ['datetime timestamp'] ) {
			$convfunc = 'convertToDateTime';
			$ctype = 4;
		} else {
			$convfunc = 'none';
			$ctype = 0;
		}
		
		die "unsupported data type: ${$prop}{name}" if  $getfunc eq 'none';
		
		if ( $dbtype == $ctype or $convfunc eq 'none' ) {
			$str = $str."obj.set".${$prop}{name}."($getfunc);\n";
		} else {
			$str = $str."obj.set".${$prop}{name}."($convfunc($getfunc));\n";
		}
	} # foreach

	return $str;
}

sub generate_class_source_file   # generate .cpp file
{
	my ($dir, $ns, $cls, $header, $cat, $tab, $props) = @_;
	
	my $ns_begin = '// BEGIN GLOBAL NAMESPACE';
	my $ns_end = '// END GLOBAL NAMESPACE';
	
	if ( defined($ns) && $ns ne "" ) {
		$ns_begin = 'namespace '.$ns."\n{";
		$ns_begin =~ s/::/\n{\nnamespace /g;
		$ns_end = $ns_begin;
		$ns_end =~ s/{/}/g;
		$ns_end =~ s/namespace.*\n//g;
		$ns_end = $ns_end.'// END OF NAMESPACE '.$ns;
	}
	
	my $daocls = $cls.'DAO';
	my $path = $dir.'/'.$cls.'DAO.cpp';
	my $table;
	if ( defined($cat) && $cat ne "") {
		$table = $cat.'.'.$tab;
	} else {
		$table = $tab;
	}
	
	my $code = "";
	my $value = "";
	my $count = 0;
	
	open my $fd, ">", $path or die "can't open file $path, $!\n";
	
	$code = $template_cpp."\n";
	$code =~ s/\^{BEGIN_NAMESPACE}/$ns_begin/g;
	$code =~ s/\^{END_NAMESPACE}/$ns_end/g;
	$code =~ s/\^{CLS_HEADER}/$header/g;
	$code =~ s/\^{DAO_CLASS}/$daocls/g;
	$code =~ s/\^{CLASS}/$cls/g;
	$code =~ s/\^{TABLE}/$table/g;
	$value = generate_db_cols_string($props);
	$code =~ s/\^{FIELDS}/$value/g;
	$value = generate_db_cols_string_no_pk($props);
	$code =~ s/\^{FIELDS_NO_PK}/$value/g;
	$value = get_db_col_pk($props);
	$code =~ s/\^{FIELD_PK}/$value/g;
	($value, $count) = get_value_holders($props, 0, 0); # all fields holders include pk
	$code =~ s/\^{INSERT_VALUES_HOLDER}/$value/g;
	($value, $count) = get_value_holders($props, 1, 0); # only pk holder
	$code =~ s/\^{SELECT_VALUE_PK_HOLDER}/$value/g;
	$code =~ s/\^{DELETE_VALUE_PK_HOLDER}/$value/g;
	($value, $count) = get_value_update_set_holders($props, -1, 0);  # no pk
	$code =~ s/\^{UPDATE_SET_NO_PK_HOLDER}/$value/g;
	($value, $count) = get_value_holders($props, 1, $count);
	$code =~ s/\^{UPDATE_PK_HOLDER}/$value/g;	
	$value = get_stream_in_fields($props, 0);
	$code =~ s/\^{STREAM_OUT_FIELDS_ALL}/$value/g;
	$value = get_stream_in_fields($props, 1);   # only pk
	$code =~ s/\^{STREAM_OUT_FIELDS_PK}/$value/g;
	$value = get_stream_in_fields($props, -1);
	$code =~ s/\^{STREAM_OUT_FIELDS_NO_PK}/$value/g;
	$value = get_stream_in_fields($props, 1);
	$code =~ s/\^{STREAM_OUT_FIELDS_PK}/$value/g;
	$value = get_values_from_stream($props, -1);
	$code =~ s/\^{GET_VALUES_FROM_STREAM_NO_PK}/$value/g;
	$value = get_values_from_stream($props, 0);
	$code =~ s/\^{VALUES_FROM_STREAM_ALL}/$value/g;
	print {$fd} $code;
	
	close $fd;
}

sub generate_class_header_file   # generate .h file
{
	my ($dir, $ns, $cls) = @_;
	
	my $guard = $ns.'_'.$cls.'_DAO_H';
	$guard =~ s/::/_/g;
	$guard = uc($guard);
	
	my $ns_begin = '// BEGIN GLOBAL NAMESPACE';
	my $ns_end = '// END GLOBAL NAMESPACE';
	
	if ( defined($ns) && $ns ne "" ) {
		$ns_begin = 'namespace '.$ns."\n{";
		$ns_begin =~ s/::/\n{\nnamespace /g;
		$ns_end = $ns_begin;
		$ns_end =~ s/{/}/g;
		$ns_end =~ s/namespace.*\n//g;
		$ns_end = $ns_end.'// END OF NAMESPACE '.$ns;
	}
	
	my $daocls = $cls.'DAO';
	my $path = $dir.'/'.$cls.'DAO.h';
	
	my $code = $template_header."\n"; 
	$code =~ s/\^{GUARD}/$guard/g;
	$code =~ s/\^{BEGIN_NAMESPACE}/$ns_begin/g;
	$code =~ s/\^{END_NAMESPACE}/$ns_end/g;
	$code =~ s/\^{DAO_CLASS}/$daocls/g;
	$code =~ s/\^{CLASS}/$cls/g;
	open my $fd, ">", $path or die "can't open file $path, $!\n";
	print {$fd} $code;
	close $fd;
}

sub process_xml_cls_prop
{
	my ($prop_type, $xmlarr, $prop) = @_;
	
	if ( $prop_type eq "id") {
		${$prop}{pk} = 'true';
	} else {
		${$prop}{pk} = 'false';
	}
	print $prop_type."\n";
	my $elem = ${$xmlarr}[0] ;
	${$prop}{name} = ${$elem}{name} if exists ${$elem}{name};
	${$prop}{ctype} = ${$elem}{ctype} if exists ${$elem}{ctype};
	${$prop}{column} = ${$elem}{column} if exists ${$elem}{column};
	${$prop}{type} = ${$elem}{type} if exists ${$elem}{type};
	${$prop}{size} = ${$elem}{size} if exists ${$elem}{size};
	
	return;
}

sub process_xml_classes
{
	my ($xmlarr, $dir) = @_;
#	print @{$xmlarr};

	my $classname = "";
	my $namespace = "";
	my $header    = "";
	my $catalog   = "";
	my $tablename = "";
	my @properties;

	my $prop_type = "none";
	
	foreach my $elem (@{$xmlarr}) {
		my $type = ref ($elem);
		if ( $type eq 'HASH' ) {
			$classname = ${$elem}{'name'} if exists ${$elem}{'name'};
			$namespace = ${$elem}{'namespace'} if exists ${$elem}{'namespace'};
			$catalog   = ${$elem}{'catalog'} if exists ${$elem}{'catalog'};
			$tablename = ${$elem}{'table'} if exists ${$elem}{'table'};
			$header    = ${$elem}{'header'} if exists ${$elem}{'header'};
		} elsif ( $type eq 'ARRAY') {
			my %prop;
			process_xml_cls_prop $prop_type, $elem, \%prop;
			push @properties, \%prop;
			$prop_type = "none";
		} elsif ( defined($elem) and $elem ne "") {
			$prop_type = $elem;
		}
	}
	
	generate_class_header_file $dir, $namespace, $classname;
	generate_class_source_file $dir, $namespace, $classname, $header, $catalog, $tablename, \@properties;
}

sub process_xml_tree
{
	my ($tree, $dir) = @_;
	
	my $val = @{$tree}[0];
	if ( $val ne 'everest-orm-mapping' ) { die "bad orm mapping xml root node name - $val"; }
	
	$val = @{$tree}[1];
	if ( ref($val) ne  'ARRAY' ) { return ;}
	
	my $status = "none";
	foreach my $elem ( @{$val} ) {
		my $type = ref ($elem);
		if ($type eq 'HASH') { next; }
		if ($type eq 'ARRAY') { 
			if ( $status eq 'class') { 
				process_xml_classes $elem, $dir;
				$status = 'none';
			}
			next; 
		} 
		if ($elem eq 'class' ) { 
			$status = 'class';
			next;
		}
	} # end foreach
}

sub print_xml_tree
{
	my ($tree, $prefix) =@_;
	foreach my $val ( @{$tree} ) {
		if ( ref($val) eq  'ARRAY' ) {
			print $prefix."BEGIN ARRAY\n";
			print_xml_tree($val, $prefix."\t");
			print $prefix."END ARRAY\n";
		} elsif ( ref($val) eq "HASH") {
			print($prefix."hash\n")
		} else {
			print($prefix."$val"."t \n") if ( defined($val) && $val ne "");
		}
	}	
}

#########################################################

my %opts;

getopt('d:h', \%opts);
my $xml = shift;
my $dir;

if ( exists $opts{'h'} ) {
	print_help;
	exit;
}

if ( exists $opts{'d'} ) {
	$dir = $opts{'d'};
}

my $odir = create_output_dir $dir;
my $parser = XML::Parser->new(Style => 'Tree');
my $tree = $parser->parsefile($xml);
#print_xml_tree $tree, "";
process_xml_tree $tree, $odir;

